import { batch, Signal, untracked } from "./singals";
import { buildBatchUpdate, deepClone, isPlainObject, merge, safeCall } from "./utils";

const batchUpdate = buildBatchUpdate((cb) => {
    batch(() => untracked(cb))
});

type NonFunctionKeys<T> = {
    [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

type NonFunctionProperties<T> = Pick<T, NonFunctionKeys<T>>;

export type ISetter<T> = (partial: Partial<NonFunctionProperties<T>> | ((prev: NonFunctionProperties<T>) => Partial<NonFunctionProperties<T>> | void)) => void;

type ReactiveCreator<T extends object> = (
    set: ISetter<T>,
    peek: () => T,
) => T;

export type Reactive<T> = {
    set(partial: Parameters<ISetter<T>>[0]): void
    peek(): T
    subscribe(subscriber: (current: T, prev: T) => void): () => void
} & (T extends object ? T : { value: T });

const ReactiveSymbol = Symbol('AlitaReactive');

class ShallowReactive<T> {
    _data: T;

    [ReactiveSymbol] = true;
    
    set(partial: Parameters<ISetter<T>>[0]) {
        const self = this;

        batchUpdate(() => {
            const prev = deepClone(self._data)
            let nextState: any
            if (typeof partial === 'function') {
                const n = partial(prev);
                nextState = isPlainObject(n) ? merge(prev, n) : prev;
            } else {
                nextState = merge(prev, partial);
            }

            if (isPlainObject(nextState)) {
                Object.assign(self, nextState)
            }
        });
    }

    peek() {
        return this._data;
    }
}

export function isReactive(obj: any): obj is Reactive<any> {
    return obj && obj[ReactiveSymbol] === true;
}

function $defineReactive(obj: any, target: any, notify: (current: any, prev: any) => void) {
    const propMap = new Map<string, Signal<any>>();

    const get = (key: string) => {
        if (!propMap.has(key)) {
            const value = target[key]
            propMap.set(key, new Signal(value))
        }

        const _signal = propMap.get(key)
        return _signal.value
    }

    const set = (key: string, value: any) => {
        if (Object.is(target[key], value)) {
            return;
        }

        batchUpdate(() => {
            const prev = { ...target }
            target[key] = value;
        
            if (propMap.has(key)) {
                propMap.get(key).value = value;
            } else {
                propMap.set(key, new Signal(value))
            }

            notify({ ...target }, prev)
        })
    }

    const properties = Object.keys(target).reduce((po, key) => {
        const value = target[key];
        let descriptor: PropertyDescriptor
        if (typeof value === 'function') {
            descriptor = { value: value.bind(obj), writable: true }
        } else {
            descriptor = {
                get: get.bind(obj, key),
                set: set.bind(obj, key)
            };
        }
        return { ...po, [key]: descriptor }
    }, {})

    Object.defineProperties(obj, properties)
}

export function reactive<T extends object>(creater: ReactiveCreator<T>): Reactive<T>;
export function reactive<T extends object>(target: T): Reactive<T> {
    let _inited = false;
    const _pendingSetters = [];
    const _subscribers = new Set<any>();
    const _notifyQueue = [];
    let _notifyBatchPending = false;

    const _reactive = new ShallowReactive<T>();

    const peek = () => _reactive.peek();
    const set = (data: any) => {
        if (!_inited) {
            _pendingSetters.push(data);
            return;
        }

        return _reactive.set(data);
    }
    const subscribe = <T>(subscriber: (current: T, prev: T) => void) => {
        _subscribers.add(subscriber);
        return () => {
            _subscribers.delete(subscriber);
        }
    }

    const $notify = <T>(current: T, prev: T) => {
        _notifyQueue.push([current, prev]);
        if (!_notifyBatchPending) {
            _notifyBatchPending = true;
            Promise.resolve().then(() => {
                const snap = _notifyQueue.splice(0, _notifyQueue.length);
                _notifyBatchPending = false;
                
                const first = snap.shift()
                let last = snap.pop()
                if (!last) {
                    last = first;
                }

                const [current, prev] = [last[0], first[1]];
                if (!Object.is(current, prev)) {
                    _subscribers.forEach((subscriber) => {
                        safeCall(subscriber, this, current, prev)
                    })
                }
            })
        }
    }

    let _data = typeof target === 'function' ? (target as ReactiveCreator<T>)(set, peek) : target;
    if (!isPlainObject(_data)) {
        console.warn('%c reactive: initializer must be a plain object', 'color: red')
        _data = { value: _data } as any
    }

    $defineReactive(_reactive, _data, $notify);

    Object.defineProperties(_reactive, {
        subscribe: {
            value: subscribe,
            writable: true,
            configurable: false,
            enumerable: true,
        },
        _data: {
            get: () => _data,
            configurable: false,
            enumerable: true,
        }
    })

    _inited = true;
    if (_pendingSetters.length) {
        _pendingSetters.forEach((data) => {
            _reactive.set(data);
        })
        _pendingSetters.length = 0;
    }

    return _reactive as never;
}